package com.zhuangjie.security.helper;

import com.zhaungjie.base.utils.StringUtils;
import com.zhuangjie.security.entity.AuthenticationTokens;
import com.zhuangjie.security.exception.NotFoundTokenException;
import com.zhuangjie.security.exception.TokenInvalidException;
import com.zhuangjie.security.helper.config.JwtHelperConfig;
import com.zhuangjie.security.helper.config.td.AllTokenDataKeys;
import com.zhuangjie.security.helper.config.td.UserConfigTokenDateKeys;
import com.zhuangjie.security.utils.JwtUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

@Component
@Slf4j
@EnableConfigurationProperties(JwtHelperConfig.class)
public class JwtHelper {
    @Autowired(required = false)
    public RedisTemplate redisTemplate;

    public static JwtHelperConfig jwtHelperConfig;

    @Autowired
    public ApplicationContext applicationContext;

    @PostConstruct
    public void setJwtHelperConfig() {
        JwtHelper.jwtHelperConfig = applicationContext.getBean(JwtHelperConfig.class);
    }

    /**
     * 令牌数据
     *
     * @author zhuangjie
     * @date 2023/10/31
     */
    @FunctionalInterface
    public interface TokenDataBuilder {
        Map<String, Object> setter(UserConfigTokenDateKeys tokenDateKeys);
    }



    /**
     * 创建令牌后登录
     * 用户信息由框架使用者认证完后调用此方法创建令牌（可进行状态控制）
     *
     * @param dataTokenConfiguration 数据令牌配置
     * @return {@link AuthenticationTokens}
     * @throws Exception 异常
     */
    public AuthenticationTokens loginAfterCreateToken(TokenDataBuilder dataTokenConfiguration) throws Exception {
        Map<String, Object> tokenData = dataTokenConfiguration.setter(new UserConfigTokenDateKeys());
        String userId = String.valueOf(tokenData.get(AllTokenDataKeys.USER_ID_KEY));
        if (userId == null) {
            throw new Exception("在生成token时，没有预设置指定唯一标记userId, 导致无法生成token !");
        }
        AuthenticationTokens authenticationTokens = createAuthenticationTokens(tokenData, true);
        // 将状态保存到redis
        if (jwtHelperConfig.enableStateControl) {
            tokenStateControl(userId, authenticationTokens.access_token);
        }
        return authenticationTokens;
    }

    /**
     * 创建身份验证令牌
     *
     * @param tokenData             令牌数据
     * @param isRequireStateControl 是否需要状态控制
     * @return {@link AuthenticationTokens}
     */
    public AuthenticationTokens createAuthenticationTokens(Map<String, Object> tokenData, Boolean isRequireStateControl) {
        if (tokenData == null) tokenData = new HashMap<>();
        // 添加模块名到token中,好判断请求的类型以此进行权限判断（如果没有，会添加，有就是自定义了）
        tokenData.computeIfAbsent(AllTokenDataKeys.MODULE_NAME_KEY, k -> jwtHelperConfig.moduleName);
        // 添加状态控制要求
        tokenData.put(AllTokenDataKeys.IS_REQUIRE_STATE_CONTROL, isRequireStateControl);
        // 将tokenData用于创建token
        String accessToken = JwtUtils.createToken(tokenData, jwtHelperConfig.accessTimeoutExpireTime, jwtHelperConfig.timeUnit);
        String refreshToken = JwtUtils.createToken(tokenData, jwtHelperConfig.refreshTimeoutExpireTime, jwtHelperConfig.timeUnit);
        // 创建AuthenticationTokens
        long refreshTokenExpireIn = System.currentTimeMillis() + jwtHelperConfig.timeUnit.toMillis(jwtHelperConfig.refreshTimeoutExpireTime);
        AuthenticationTokens authenticationTokens = AuthenticationTokens.builder()
                .access_token(accessToken)
                .refresh_token(refreshToken)
                .refresh_token_expire_in(refreshTokenExpireIn)
                .build();
        return authenticationTokens;
    }

    /**
     * 刷新令牌(需要传入refresh_token)
     *
     * @param refreshToken 刷新令牌
     * @return {@link AuthenticationTokens}
     * @throws Exception 例外
     */
    public AuthenticationTokens refreshToken(String refreshToken) throws Exception {
        Map<String, Object> tokenData = JwtUtils.checkToken(refreshToken);

        Object accessTokenOfRedis = redisTemplate.opsForValue().get(jwtHelperConfig.loginStateKeyValueOf(String.valueOf(jwtHelperConfig.moduleName)) + tokenData.get(AllTokenDataKeys.USER_ID_KEY));
        if (accessTokenOfRedis != null) {
            // 如果有现存的token，直接作为新的access_token,这样做是为解决前端可能多次请求刷新token
            try {
                JwtUtils.checkToken((String) accessTokenOfRedis);
                log.info("已有存在的access_token且有效,将不使用新的!");
                AuthenticationTokens newAuthenticationTokens = createAuthenticationTokens(tokenData, false);
                return AuthenticationTokens.builder()
                        .access_token((String) accessTokenOfRedis)
                        .refresh_token(newAuthenticationTokens.refresh_token)
                        .refresh_token_expire_in(newAuthenticationTokens.refresh_token_expire_in)
                        .build();
            }catch (Exception e) {}
        }
        // 找出当前在当前用户在redis中的toekn是否还有效，如果还有效直接直接作为token返回
        return loginAfterCreateToken((userConfigTokenDateKeys) -> tokenData);
    }

    /**
     * 登录成功后，保存用户的登录状态
     *
     * @param id
     * @param accessToken
     */
    protected void tokenStateControl(String id, String accessToken) {
        if (id == null || accessToken == null) return;
        redisTemplate.opsForValue().set(jwtHelperConfig.loginStateKey + id, accessToken, jwtHelperConfig.accessTimeoutExpireTime, jwtHelperConfig.timeUnit);
    }

    /**
     * 可用于管理员让用户强制下线
     *
     * @param id
     */
    public void offline(String id) {
        if (jwtHelperConfig.enableStateControl) {
            return;
        }
        redisTemplate.delete(jwtHelperConfig.loginStateKey + id);
    }

    public Object getTokenOwnerId(String token) throws Exception {
        Map<String, Object> tokenData = JwtUtils.checkToken(token);
        return tokenData.get(AllTokenDataKeys.USER_ID_KEY);
    }

    /**
     * 检查用户提交的token是否有效，会解决传过来的token进行检查并获取里面的id从redis中获取是否有记录，并看value值是否是用户传过来的token
     * 只有这样用户传过来的token才是有效的，并返回token解析出来的信息。
     *
     * @param token
     * @return {@link Map}<{@link String}, {@link Object}>
     * @throws Exception
     */
    public Map<String, Object> parseToken(String token, Boolean isAllowOtherModulesToken, Boolean isNotCheckState) throws Exception {
        if (StringUtils.isBlank(token)) return null;
        // 解析token
        Map<String, Object> toeknData;
        try {
            toeknData = JwtUtils.checkToken(token);
            if (toeknData == null) throw new Exception();
        } catch (Exception e) {
            throw new Exception("Token过期！");
        }
        // token验证
        // - 如果指定不校验或token不支持校验则就不要进行状态验证
        if (isNotCheckState || Objects.equals(toeknData.get(AllTokenDataKeys.IS_REQUIRE_STATE_CONTROL), false)) {
            // 如果不检查状态，直接返回解析的tokenData
            return toeknData;
        }
        // - 到这里说明需要状态验证
        if (!jwtHelperConfig.enableStateControl) {
            // 如果当前不具有检查token状态的能力，但又需要检查，则返回null
            throw new Exception("安全框架：当前模块‘" + jwtHelperConfig.moduleName + "’不具有验证token状态的能力！");
        }
        Object tokenModuleName = toeknData.get(AllTokenDataKeys.MODULE_NAME_KEY);
        Boolean isCurrentModuleToken = Objects.equals(tokenModuleName, jwtHelperConfig.moduleName);
        // 如果解析的不是当前模块颁发的token且不允许其它模块的token且返回null
        if (!isCurrentModuleToken && !isAllowOtherModulesToken) {
            throw new Exception("安全框架：token错误，不允许其它模块的token！");
        }

        // 到这里是需要检查token状态，且当前模块具有token的能力
        String tokenUserId = String.valueOf(toeknData.get(AllTokenDataKeys.USER_ID_KEY));
        Object tokenOfRedis = redisTemplate.opsForValue().get(jwtHelperConfig.loginStateKeyValueOf(String.valueOf(tokenModuleName)) + tokenUserId);
        if (tokenOfRedis == null || !Objects.equals(tokenOfRedis, token)) {
            throw new Exception("登录状态已失效");
        }
        return toeknData;
    }


    /**
     * 从授权标头获取令牌
     *
     * @return {@link String}
     * @throws Exception 例外
     */
    public static String getTokenFromAuthorizationHeader() {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String token = request.getHeader(jwtHelperConfig.authorizationHeader);
        // 还是无法获取token，抛出异常！
        if (StringUtils.isBlank(token)) {
            return null;
        }
        // 提取token
        return extractToken(token);
    }

    /**
     * 登录状态检查（结合redis），检查用户提交过来的token是否有效，如果有效，则返回用户信息，如果无效，则返回null
     *
     * @return {@link Map}<{@link String}, {@link Object}>
     * @throws Exception 异常
     */
    public Map<String, Object> authentication(String token, Boolean isParseAuthenticationHeader, Boolean isNotCheckState, Boolean isAllowOtherModulesToken) throws Exception {
        // 没有传入token 且 (不允许从请求头中获取 or 无法获取到 )
        if (StringUtils.isBlank(token) && (!isParseAuthenticationHeader || StringUtils.isBlank(token = getTokenFromAuthorizationHeader()))) {
            throw new NotFoundTokenException("安全框架：在认证token时，无法获取到token！没有传入token，且无法从请求头中获取到！");
        }
        // 解析token
        try {
            return parseToken(token, isAllowOtherModulesToken, isNotCheckState);
        }catch (Exception e) {
            throw new TokenInvalidException("安全框架：token无效，token可能失效或不是需要颁发核模块token！");
        }
    }

    /**
     * 提取令牌
     *
     * @param tokenFormatString 令牌格式字符串
     * @return {@link String}
     */
    public static String extractToken(String tokenFormatString) {
        if (StringUtils.isBlank(tokenFormatString)) {
            return null;
        }
        return tokenFormatString.replaceFirst("^Bearer\\s*", "");
    }

}
